Перейти к основному содержимому

Разработка игр на Python

Разработчику Архитектору

Разработка игр на Python

Основы разработки игр на Python

Python не является основным языком в индустрии разработки игр. Его динамическая типизация, интерпретируемая природа и относительно низкая производительность делают его малопригодным для создания ресурсоёмких проектов — таких как AAA-игры или масштабные многопользовательские онлайн-платформы. Однако Python остаётся мощным инструментом в контексте обучения, прототипирования, скриптования игровой логики и реализации простых 2D-приложений.

Для этих целей существует ряд библиотек, позволяющих организовать графический вывод, обработку ввода, аудиосопровождение и базовую физику. Наиболее известной и долгоживущей из них является Pygame — кроссплатформенная библиотека, построенная на основе SDL (Simple DirectMedia Layer). Она предоставляет минимально необходимый набор средств для создания 2D-игр и используется преимущественно в образовательных целях, а также в инди-проектах небольшого масштаба.

Python не предназначен для создания игровых движков уровня Unreal Engine или Unity. Тем не менее, он может эффективно применяться как средство обучения основам геймдева, анализа алгоритмов поведения персонажей, реализации ИИ в играх, а также как платформа для быстрого прототипирования механик.

Что касается других технологий, то Godot поддерживает язык GDScript, синтаксически близкий к Python, но это не Python. Начиная с версии 4.0, Godot также предлагает официальную поддержку Python через модуль GDExtension (на базе Cython), однако использование Python в Godot — скорее исключение, чем правило. Основная экосистема Godot ориентирована на GDScript, C# и C++. Таким образом, утверждение о том, что Godot «использует Python» — упрощение, не соответствующее действительности в полной мере.

В рамках данной главы мы сосредоточимся на Pygame — наиболее доступной и документированной библиотеке для разработки 2D-игр на чистом Python.

Pygame — это набор модулей Python, предоставляющий доступ к низкоуровневым функциям мультимедиа через привязки к библиотеке SDL. Он позволяет работать с графикой, звуком, вводом с клавиатуры, мыши и геймпадов, а также обеспечивает базовые средства для обработки времени и столкновений.

Библиотека не является игровым движком в строгом смысле слова. У неё отсутствуют встроенные системы анимаций, физики, сцен, шейдеров или редактор ресурсов. Вместо этого Pygame предоставляет низкоуровневые примитивы, которые разработчик должен компоновать самостоятельно, реализуя игровую логику «с нуля».

Тем не менее, именно эта особенность делает Pygame ценным инструментом для обучения: он заставляет разработчика понимать внутренние механизмы работы игрового цикла, обработки событий и управления состоянием объектов.


Pygame

Pygame представляет собой набор модулей Python, предоставляющий доступ к низкоуровневым функциям мультимедиа через привязки к библиотеке SDL (Simple DirectMedia Layer). Библиотека обеспечивает работу с графикой, звуком, вводом с клавиатуры, мыши и геймпадов. Система включает средства обработки времени и обнаружения столкновений.

Библиотека строится на основе SDL, написанной на языке C. Python-код выступает обёрткой, передающей команды в низкоуровневую среду. Это определяет производительность и кроссплатформенность. Поддерживаются операционные системы Windows, macOS, Linux.

Установка выполняется через менеджер пакетов pip:

pip install pygame

Проверка установки возможна через вывод версии модуля:

import pygame
print(pygame.ver)

Любое приложение на Pygame содержит базовый набор вызовов. Отсутствие любого из них приводит к ошибке выполнения или некорректной работе графического интерфейса.

  1. Импорт модуля: import pygame.
  2. Инициализация: pygame.init(). Запускает внутренние подсистемы (видео, аудио, ввод).
  3. Создание окна: pygame.display.set_mode(). Возвращает объект поверхности экрана.
  4. Игровой цикл: Бесконечный цикл while, обрабатывающий события и обновляющий кадр.
  5. Завершение: pygame.quit() и выход из интерпретатора.

Пример минимальной структуры:

import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Окно игры")

running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

screen.fill((0, 0, 0))
pygame.display.flip()

pygame.quit()

Библиотека разделена на логические модули. Каждый модуль отвечает за конкретную задачу.

МодульНазначениеКлючевые объекты
pygame.displayУправление окном и экраномSurface, set_mode, flip, update
pygame.drawРисование примитивовcircle, rect, line, polygon
pygame.fontРабота со шрифтами и текстомFont, render, SysFont
pygame.eventОбработка событий вводаget, poll, QUIT, KEYDOWN
pygame.timeКонтроль времени и кадровClock, tick, get_ticks
pygame.imageЗагрузка и сохранение изображенийload, save, fromstring
pygame.mixerВоспроизведение звукаSound, music, play, stop
pygame.spriteУправление игровыми объектамиSprite, Group, collide
pygame.rectПрямоугольники и коллизииRect, colliderect, move
pygame.keyСостояние клавиатурыget_pressed, name
pygame.mouseСостояние мышиget_pos, get_pressed, set_visible

Вызов pygame.init() активирует все доступные модули. Существует возможность инициализации отдельных подсистем для экономии ресурсов, например pygame.display.init() или pygame.mixer.init(). Функция pygame.get_init() возвращает статус инициализации. Функция pygame.quit() выключает все модули.


Простое управление спрайтом

Чтобы реализовать простейшее управление через W, A, S, D, в Pygame необходимо выполнить следующие шаги:

  1. Создать класс, наследуемый от pygame.sprite.Sprite.
  2. Инициализировать изображение (или цветной прямоугольник для прототипа) и задать его позицию (rect).
  3. Обработать события клавиатуры (KEYDOWN и KEYUP) для отслеживания нажатия и отпускания клавиш.
  4. В методе обновления (update) изменять координаты спрайта в зависимости от текущего состояния клавиш.
  5. Добавить проверку границ экрана, чтобы спрайт не уходил за пределы видимой области.

Ниже приведен полный, рабочий пример кода. Вы можете сохранить его в файл main.py и запустить.

import pygame
import sys

# Инициализация Pygame
pygame.init()

# Константы конфигурации
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FPS = 60
SPEED = 5

# Цвета (R, G, B)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)

# Настройка экрана
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("WASD Movement Example")
clock = pygame.time.Clock()

class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
# Создаем спрайт: можно использовать surface или rect
self.image = pygame.Surface((40, 40))
self.image.fill(RED)

# Получаем прямоугольник вокруг изображения
self.rect = self.image.get_rect()

# Начальная позиция (центр экрана)
self.rect.centerx = SCREEN_WIDTH // 2
self.rect.centery = SCREEN_HEIGHT // 2

# Словарь для хранения состояния клавиш
self.keys_pressed = {
'w': False,
'a': False,
's': False,
'd': False
}

def update(self):
# Проверка нажатых клавиш
if self.keys_pressed['w'] and self.rect.top > 0:
self.rect.y -= SPEED
if self.keys_pressed['s'] and self.rect.bottom < SCREEN_HEIGHT:
self.rect.y += SPEED
if self.keys_pressed['a'] and self.rect.left > 0:
self.rect.x -= SPEED
if self.keys_pressed['d'] and self.rect.right < SCREEN_WIDTH:
self.rect.x += SPEED

def main():
all_sprites = pygame.sprite.Group()
player = Player()
all_sprites.add(player)

running = True
while running:
# Обработка событий
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Отслеживание нажатия клавиш
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_w:
player.keys_pressed['w'] = True
elif event.key == pygame.K_a:
player.keys_pressed['a'] = True
elif event.key == pygame.K_s:
player.keys_pressed['s'] = True
elif event.key == pygame.K_d:
player.keys_pressed['d'] = True

# Отслеживание отпускания клавиш
elif event.type == pygame.KEYUP:
if event.key == pygame.K_w:
player.keys_pressed['w'] = False
elif event.key == pygame.K_a:
player.keys_pressed['a'] = False
elif event.key == pygame.K_s:
player.keys_pressed['s'] = False
elif event.key == pygame.K_d:
player.keys_pressed['d'] = False

# Обновление логики
all_sprites.update()

# Отрисовка
screen.fill(BLACK)
all_sprites.draw(screen)

# Перерисовка экрана
pygame.display.flip()

# Ограничение FPS
clock.tick(FPS)

pygame.quit()
sys.exit()

if __name__ == "__main__":
main()

Ключевые моменты реализации:

  1. Состояние клавиш: Вместо прямого изменения координат внутри обработчика событий используется словарь keys_pressed. Это позволяет реализовать плавное движение при одновременном нажатии нескольких клавиш (например, диагональ W+A). Если менять координаты только в событии KEYDOWN, движение будет рывками.
  2. Границы экрана: В методе update добавлены проверки (self.rect.top > 0 и т.д.), чтобы спрайт не выходил за пределы игрового поля.
  3. Оптимизация: Использование pygame.sprite.Group упрощает управление несколькими объектами, если в будущем потребуется добавить врагов или препятствия.
  4. Объективность: Код использует стандартные возможности Pygame без сторонних библиотек, что гарантирует совместимость и предсказуемость поведения.

Если вам потребуются более сложные механики (столкновения, анимация, использование изображений .png вместо цветных квадратов), это можно реализовать путем загрузки изображения через pygame.image.load() и добавления методов обработки коллизий.


Окно и поверхности (Surfaces)

Модуль display контролирует графическое окно. Центральным объектом выступает поверхность (Surface), возвращаемая функцией set_mode().

Функция set_mode(size, flags=0, depth=0) принимает кортеж размеров (ширина, высота). Флаги определяют режим отображения.

ФлагОписание
pygame.FULLSCREENРазворачивает окно на весь экран
pygame.RESIZABLEПозволяет пользователю менять размер окна
pygame.NOFRAMEУбирает рамку и заголовок окна
pygame.HIDDENСоздаёт окно в скрытом режиме

Центральным понятием в Pygame является поверхность (Surface) — двумерный массив пикселей, представляющий собой область рисования. Каждое изображение, текст или графический элемент в Pygame является экземпляром pygame.Surface.

# Создание новой поверхности размером 100x100 с прозрачностью
surf = pygame.Surface((100, 100), pygame.SRCALPHA)
surf.fill((255, 0, 0, 128)) # Красный цвет с полупрозрачностью

Главное окно игры создаётся с помощью pygame.display.set_mode(), которое возвращает объект Surface, связанный с экраном. Все последующие операции рисования (blitting) выполняются на этой или других поверхностях.

import pygame
screen = pygame.display.set_mode((800, 600))

Поверхности могут быть прозрачными (с альфа-каналом), иметь цветовой ключ (colorkey) для маскирования фона, а также подвергаться трансформациям: масштабированию, повороту, отражению.

Изменения на поверхности не отображаются автоматически. Требуется явный вызов функции обновления.

ФункцияОписание
pygame.display.flip()Обновляет весь экран. Подходит для двойной буферизации
pygame.display.update(rectangle)Обновляет указанную область экрана. Экономит ресурсы

Заголовок и иконка тоже могут быть изменены:

pygame.display.set_caption("Название игры")
icon = pygame.image.load("icon.png")
pygame.display.set_icon(icon)

Координатная система

В Pygame начало координат (0, 0) находится в левом верхнем углу экрана. Ось X направлена вправо. Ось Y направлена вниз.

Это отличается от стандартной математической системы, где Y направлен вверх. При расчёте физики необходимо учитывать инверсию оси Y.

Размеры окна задаются в пикселях. Координаты объектов хранятся в целых числах. Дробные координаты возможны внутри логики игры, но отрисовка требует целочисленных значений.


Рисование

Модуль draw содержит функции для рисования геометрических фигур непосредственно на поверхности.

ФункцияПараметрыОписание
rectsurface, color, rect, width=0Рисует прямоугольник. width=0 заполняет фигуру
polygonsurface, color, points, width=0Рисует многоугольник по списку точек
circlesurface, color, center, radius, width=0Рисует круг
ellipsesurface, color, rect, width=0Рисует эллипс внутри прямоугольника
arcsurface, color, rect, start_angle, stop_angle, width=1Рисует дугу
linesurface, color, start_pos, end_pos, width=1Рисует линию между двумя точками
linessurface, color, closed, points, width=1Рисует набор соединённых линий
aalinesurface, color, start_pos, end_pos, blend=1Рисует сглаженную линию
aalinessurface, color, closed, points, blend=1Рисует набор сглаженных линий

Пример рисования фигуры:

pygame.draw.rect(screen, (0, 255, 0), (50, 50, 100, 100))
pygame.draw.circle(screen, (0, 0, 255), (200, 200), 50)

Класс pygame.Rect хранит координаты и размеры прямоугольной области. Используется для позиционирования и проверки столкновений.

# Из координат и размеров
rect = pygame.Rect(x, y, width, height)
# Из поверхности
rect = surface.get_rect()
# Из кортежа
rect = pygame.Rect((10, 10, 50, 50))

Объект Rect обладает набором атрибутов для доступа к координатам сторон и углов.

АтрибутОписание
x, yКоординаты левого верхнего угла
width, heightРазмеры прямоугольника
top, left, bottom, rightКоординаты сторон
center, centerx, centeryКоординаты центра
topleft, topright, bottomleft, bottomrightКоординаты углов
midtop, midleft, midbottom, midrightКоординаты середин сторон
sizeКортеж (width, height)
w, hКороткие aliases для width и height

Методы Rect:

МетодОписание
colliderect(other_rect)Возвращает True при пересечении с другим Rect
collidepoint(x, y)Возвращает True, если точка внутри Rect
collidelist(list)Проверяет пересечение со списком Rect
move(x, y)Возвращает новый Rect, смещённый на x, y
move_ip(x, y)Смещает текущий Rect на x, y (in place)
clamp(other_rect)Перемещает Rect внутрь другого Rect
clip(other_rect)Возвращает пересечение двух Rect
union(other_rect)Возвращает объединение двух Rect
unionall(list)Возвращает объединение со списком Rect
fit(other_rect)Масштабирует и перемещает Rect внутрь другого
normalize()Исправляет отрицательную ширину или высоту

Текст и шрифты (font)

Модуль font позволяет рендерить текст на поверхностях. Текст в Pygame является изображением.

# Системный шрифт
font = pygame.font.SysFont("Arial", 24)
# Загрузка из файла
font = pygame.font.Font("file.ttf", 24)
# Шрифт по умолчанию
font = pygame.font.Font(None, 24)

Метод render(text, antialias, color, background=None) создаёт поверхность с текстом.

ПараметрОписание
textСтрока для отображения
antialiasBoolean. Сглаживание границ букв
colorЦвет текста (RGB или RGBA)
backgroundЦвет фона текста (опционально)

Пример вывода текста:

text_surface = font.render("Счёт: 10", True, (255, 255, 255))
screen.blit(text_surface, (10, 10))

Доступные свойства Font:

СвойствоОписание
get_height()Высота символов в пикселях
get_linesize()Высота строки
get_ascent()Подъём шрифта над базовой линией
get_descent()Спуск шрифта под базовую линию
size(text)Возвращает кортеж (width, height) текста
metrics(text)Возвращает метрики для каждого символа
set_bold(bool)Устанавливает жирное начертание
set_italic(bool)Устанавливает курсив
set_underline(bool)Устанавливает подчёркивание
get_bold(), get_italic(), get_underline()Возвращают статус стилей

События и ввод (event)

Система событий обрабатывает действия пользователя и системы. События помещаются в очередь.

for event in pygame.event.get():
# Обработка

Функция pygame.event.get() возвращает список событий за последний кадр. Функция pygame.event.poll() возвращает одно событие или pygame.NOEVENT.

Типы событий:

ТипОписание
pygame.QUITПользователь нажал кнопку закрытия окна
pygame.KEYDOWNКлавиша нажата
pygame.KEYUPКлавиша отпущена
pygame.MOUSEMOTIONДвижение мыши
pygame.MOUSEBUTTONDOWNНажатие кнопки мыши
pygame.MOUSEBUTTONUPОтпускание кнопки мыши
pygame.JOYAXISMOTIONДвижение оси геймпада
pygame.JOYBUTTONDOWNНажатие кнопки геймпада

Событие KEYDOWN содержит атрибут key (код клавиши) и mod (модификаторы Shift, Ctrl).

if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
jump()
if event.key == pygame.K_ESCAPE:
running = False

Постоянное состояние клавиш проверяется через pygame.key.get_pressed(). Возвращает кортеж булевых значений для всех клавиш.

keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
player.x -= speed

Событие MOUSEBUTTONDOWN содержит атрибут button (1 - левая, 3 - правая, 2 - колесо). Атрибут pos содержит координаты (x, y).

if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
shoot(event.pos)

Текущая позиция мыши: pygame.mouse.get_pos(). Состояние кнопок: pygame.mouse.get_pressed().

Для геймпадов же требуется инициализация модуля pygame.joystick.

pygame.joystick.init()
joystick_count = pygame.joystick.get_count()
if joystick_count > 0:
joystick = pygame.joystick.Joystick(0)
joystick.init()

В цикле событий обрабатываются JOYAXISMOTION и JOYBUTTONDOWN. Оси возвращают значения от -1.0 до 1.0.


Время и частота кадров (time)

Модуль time контролирует скорость игры.

Объект pygame.time.Clock() регулирует частоту кадров.

clock = pygame.time.Clock()
# В конце цикла
clock.tick(60) # Ограничивает цикл 60 кадрами в секунду

Метод tick(framerate) задерживает выполнение, чтобы достичь указанной частоты. Возвращает время в миллисекундах с прошлого вызова.

Функция pygame.time.get_ticks() возвращает время в миллисекундах с момента инициализации pygame.init(). Используется для таймеров.

start_time = pygame.time.get_ticks()
# В цикле
current_time = pygame.time.get_ticks()
if current_time - start_time > 1000:
print("Прошла секунда")
start_time = current_time

Функция pygame.time.delay(ms) приостанавливает программу на указанное время. pygame.time.wait(ms) работает аналогично, но блокирует события.


Изображения и звук

Модуль pygame.image загружает файлы в поверхности.

image = pygame.image.load("player.png")
# Конвертация формата для скорости
image = image.convert()
# Конвертация с прозрачностью
image = image.convert_alpha()

Поддерживемые форматы: PNG, JPG, BMP, GIF.

Модуль pygame.mixer управляет аудио.

Класс/ФункцияОписание
pygame.mixer.Sound(file)Загружает звуковой эффект в память
sound.play()Воспроизводит звук
sound.stop()Останавливает звук
sound.set_volume(val)Устанавливает громкость (0.0 - 1.0)
pygame.mixer.music.load(file)Загружает фоновую музыку (поток)
pygame.mixer.music.play(loops)Запускает музыку. loops=-1 для бесконечности
pygame.mixer.music.stop()Останавливает музыку
pygame.mixer.music.set_volume(val)Громкость музыки

Пример использования:

jump_sound = pygame.mixer.Sound("jump.wav")
jump_sound.play()

pygame.mixer.music.load("bgm.mp3")
pygame.mixer.music.play(-1)

Цикл игры (Game Loop)

Игровой цикл — фундаментальная структура любой интерактивной программы. В Pygame он реализуется вручную и состоит из трёх этапов:

  • Обработка событий (Event Handling)
  • Обновление состояния игры (State Update)
  • Отрисовка (Rendering) Пример базового цикла:
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Обновление логики
update_game_state()

# Отрисовка
screen.fill((0, 0, 0))
draw_objects(screen)
pygame.display.flip()

Цикл выполняется с максимально возможной частотой, обычно ограниченной с помощью pygame.time.Clock, чтобы обеспечить стабильный FPS (например, 60 кадров в секунду).


События (Events)

Pygame использует систему очереди событий. Все внешние воздействия — нажатия клавиш, движения мыши, закрытие окна — помещаются в очередь и обрабатываются в цикле через pygame.event.get().

События являются объектами с атрибутами type (тип события) и, опционально, дополнительными данными (key, pos, button и т.д.). Это позволяет реализовать реактивную архитектуру, где игра реагирует на пользовательский ввод асинхронно.

for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
player.jump()

Спрайты (Sprites)

Pygame использует объектно-ориентированную структуру. Использование классов упрощает управление множеством объектов. Класс наследуется от pygame.sprite.Sprite.

Базовый класс спрайта:

class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((50, 50))
self.image.fill((0, 0, 255))
self.rect = self.image.get_rect()
self.rect.center = (400, 300)
self.speed = 5

def update(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT]:
self.rect.x += self.speed
if keys[pygame.K_UP]:
self.rect.y -= self.speed
if keys[pygame.K_DOWN]:
self.rect.y += self.speed

Спрайт в Pygame — это визуальный объект, обычно наследующий от класса pygame.sprite.Sprite. Спрайты объединяют графическое представление (Surface) и положение (Rect), а также могут содержать логику поведения.

Pygame предоставляет систему групп (pygame.sprite.Group) для эффективного управления коллекциями спрайтов: отрисовки, обновления, проверки столкновений.

class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((50, 50))
self.image.fill((255, 0, 0))
self.rect = self.image.get_rect()
self.rect.center = (400, 300)

def update(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
self.rect.x -= 5

Группировка спрайтов позволяет отделить логику от представления и упрощает управление сотнями объектов.

Класс pygame.sprite.Group хранит списки спрайтов. Позволяет обновлять и отрисовывать все объекты одним вызовом.

all_sprites = pygame.sprite.Group()
player = Player()
all_sprites.add(player)

# В цикле
all_sprites.update()
all_sprites.draw(screen)

Группы поддерживают проверку столкновений.

hits = pygame.sprite.spritecollide(player, enemies, False)

Параметр False указывает не удалять спрайт при столкновении.


Столкновения (Collisions)

Pygame предоставляет несколько методов для обнаружения пересечений между прямоугольниками (pygame.Rect). Основные функции:

  • rect1.colliderect(rect2) — проверяет пересечение двух прямоугольников.
  • pygame.sprite.collide_rect(sprite1, sprite2) — то же, для спрайтов.
  • pygame.sprite.spritecollide(sprite, group, dokill) — проверяет столкновение спрайта с группой.

Для более сложных форм используются маски (pygame.mask.from_surface()), обеспечивающие пиксельно-точное обнаружение столкновений, хотя и с большими вычислительными затратами.


Звуки и музыка

Pygame поддерживает воспроизведение аудиофайлов через модули pygame.mixer и pygame.mixer.music.

  • pygame.mixer.Sound — для коротких эффектов (выстрел, прыжок).
  • pygame.mixer.music — для фоновой музыки (поддерживает потоковое воспроизведение).
jump_sound = pygame.mixer.Sound("jump.wav")
jump_sound.play()

pygame.mixer.music.load("background.mp3")
pygame.mixer.music.play(-1) # зацикливание

Поддерживаются форматы WAV, MP3, OGG. Однако качество и стабильность воспроизведения зависят от платформы и бэкенда SDL.

Пример: реализация «Змейки». Это классический пример, демонстрирующий работу с циклом, вводом, отрисовкой и логикой столкновений.

Основные компоненты:

  • Состояние змейки: список координат её сегментов.
  • Направление движения: вектор (dx, dy).
  • Еда: случайно генерируемая позиция.
  • Условия завершения: выход за границы поля или самопересечение.

Полный код:

import pygame
import random

pygame.init()

white = (255, 255, 255)
black = (0, 0, 0)
red = (213, 50, 80)
green = (0, 255, 0)
blue = (50, 153, 213)

width = 600
height = 400

screen = pygame.display.set_mode((width, height))
pygame.display.set_caption('Змейка')

clock = pygame.time.Clock()
snake_block = 10
snake_speed = 15

font_style = pygame.font.SysFont("bahnschrift", 25)
score_font = pygame.font.SysFont("comicsansms", 35)

def your_score(score):
value = score_font.render("Счёт: " + str(score), True, black)
screen.blit(value, [0, 0])

def our_snake(snake_block, snake_list):
for x in snake_list:
pygame.draw.rect(screen, green, [x[0], x[1], snake_block, snake_block])

def message(msg, color):
mesg = font_style.render(msg, True, color)
screen.blit(mesg, [width / 6, height / 3])

def gameLoop():
game_over = False
game_close = False

x1 = width / 2
y1 = height / 2

x1_change = 0
y1_change = 0

snake_list = []
length_of_snake = 1

foodx = round(random.randrange(0, width - snake_block) / 10.0) * 10.0
foody = round(random.randrange(0, height - snake_block) / 10.0) * 10.0

while not game_over:

while game_close:
screen.fill(white)
message("Ты проиграл! C - продолжить, Q - выйти", red)
your_score(length_of_snake - 1)
pygame.display.update()

for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_q:
game_over = True
game_close = False
if event.key == pygame.K_c:
gameLoop()

for event in pygame.event.get():
if event.type == pygame.QUIT:
game_over = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
x1_change = -snake_block
y1_change = 0
elif event.key == pygame.K_RIGHT:
x1_change = snake_block
y1_change = 0
elif event.key == pygame.K_UP:
y1_change = -snake_block
x1_change = 0
elif event.key == pygame.K_DOWN:
y1_change = snake_block
x1_change = 0

if x1 >= width or x1 < 0 or y1 >= height or y1 < 0:
game_close = True

x1 += x1_change
y1 += y1_change

screen.fill(white)
pygame.draw.rect(screen, red, [foodx, foody, snake_block, snake_block])

snake_head = [x1, y1]
snake_list.append(snake_head)

if len(snake_list) > length_of_snake:
del snake_list[0]

for x in snake_list[:-1]:
if x == snake_head:
game_close = True

our_snake(snake_block, snake_list)
your_score(length_of_snake - 1)

pygame.display.update()

if x1 == foodx and y1 == foody:
foodx = round(random.randrange(0, width - snake_block) / 10.0) * 10.0
foody = round(random.randrange(0, height - snake_block) / 10.0) * 10.0
length_of_snake += 1

clock.tick(snake_speed)

pygame.quit()
quit()

gameLoop()

Pygame — обучающая платформа, позволяющая освоить ключевые концепции геймдева: игровой цикл, обработку ввода, управление состоянием, коллизии и отрисовку. Он даёт контроль над каждым аспектом, что способствует глубокому пониманию механизмов, лежащих в основе интерактивных приложений.

Для серьёзных проектов рекомендуется использовать специализированные игровые движки: Godot, Unity, Unreal. Однако для обучения, прототипирования или реализации простых 2D-игр Python с Pygame остаётся практичным и доступным выбором.


Практические примеры реализации

Вывод текста на экран

Создание функции для централизованного вывода текста упрощает код.

def draw_text(surface, text, size, x, y):
font = pygame.font.SysFont("serif", size)
text_surface = font.render(text, True, (255, 255, 255))
text_rect = text_surface.get_rect()
text_rect.midtop = (x, y)
surface.blit(text_surface, text_rect)

Создание спрайта игрока с анимацией

Анимация реализуется через смену изображений по таймеру.

class AnimatedPlayer(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.frames = []
for i in range(4):
img = pygame.image.load(f"player_{i}.png").convert_alpha()
self.frames.append(img)
self.image = self.frames[0]
self.rect = self.image.get_rect()
self.frame_index = 0
self.last_update = 0
self.frame_rate = 100 # мс между кадрами

def update(self):
now = pygame.time.get_ticks()
if now - self.last_update > self.frame_rate:
self.last_update = now
self.frame_index = (self.frame_index + 1) % len(self.frames)
self.image = self.frames[self.frame_index]

Спрайт противника и движение

Противники двигаются автоматически вниз.

class Enemy(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((30, 30))
self.image.fill((255, 0, 0))
self.rect = self.image.get_rect()
self.rect.x = random.randrange(0, 800)
self.rect.y = random.randrange(-100, -40)
self.speed_y = random.randrange(2, 8)

def update(self):
self.rect.y += self.speed_y
if self.rect.top > 600:
self.rect.x = random.randrange(0, 800)
self.rect.y = random.randrange(-100, -40)
self.speed_y = random.randrange(2, 8)

Платформа с коллизией

Проверка столкновения игрока с платформой требует проверки направления движения.

class Platform(pygame.sprite.Sprite):
def __init__(self, x, y, w, h):
super().__init__()
self.image = pygame.Surface((w, h))
self.image.fill((100, 100, 100))
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y

# В цикле игрока
hits = pygame.sprite.spritecollide(player, platforms, False)
if hits:
if player.vel_y > 0: # Падение вниз
player.rect.bottom = hits[0].rect.top
player.vel_y = 0
player.jumping = False

Пуля или снаряд

Пули создаются при нажатии клавиши и летят вперёд.

class Bullet(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.Surface((10, 5))
self.image.fill((255, 255, 0))
self.rect = self.image.get_rect()
self.rect.center = (x, y)
self.speed = 10

def update(self):
self.rect.x += self.speed
if self.rect.left > 800:
self.kill() # Удаляет спрайт из всех групп

# Стрельба
class Player(pygame.sprite.Sprite):
def update(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE]:
bullet = Bullet(self.rect.centerx, self.rect.centery)
all_sprites.add(bullet)
bullets.add(bullet)

Бонусы и сбор предметов

Бонусы исчезают при касании игрока.

class Bonus(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((20, 20))
self.image.fill((0, 255, 0))
self.rect = self.image.get_rect()
self.rect.x = random.randrange(0, 800)
self.rect.y = random.randrange(-100, -40)
self.speed_y = 3

def update(self):
self.rect.y += self.speed_y
if self.rect.top > 600:
self.kill()

# В главном цикле
hits = pygame.sprite.spritecollide(player, bonuses, True) # True удаляет бонус
for hit in hits:
score += 10

Реакция на клик мыши

Спавн объекта в месте клика.

for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # Левая кнопка
pos = pygame.mouse.get_pos()
effect = Effect(pos[0], pos[1])
all_sprites.add(effect)

Передвижение спрайта с ускорением

Использование векторов скорости и ускорения.

class PhysicsPlayer(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((50, 50))
self.image.fill((0, 0, 255))
self.rect = self.image.get_rect()
self.vel_x = 0
self.vel_y = 0
self.accel_x = 0
self.accel_y = 0
self.friction = -0.1

def update(self):
self.accel_x = 0
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
self.accel_x = -0.5
if keys[pygame.K_RIGHT]:
self.accel_x = 0.5

self.accel_x += self.vel_x * self.friction
self.vel_x += self.accel_x
self.rect.x += self.vel_x

# Ограничение скорости
if self.vel_x > 10: self.vel_x = 10
if self.vel_x < -10: self.vel_x = -10

Вывод сообщений и экран Game Over

Отрисовка поверх игрового процесса.

def draw_game_over(screen):
font = pygame.font.SysFont("Arial", 64)
text = font.render("GAME OVER", True, (255, 0, 0))
rect = text.get_rect(center=(400, 300))
screen.blit(text, rect)

font_small = pygame.font.SysFont("Arial", 24)
text_small = font_small.render("Нажмите R для рестарта", True, (255, 255, 255))
rect_small = text_small.get_rect(center=(400, 350))
screen.blit(text_small, rect_small)

Включение и выключение звуков

Управление микшером через клавиши.

for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_m:
if pygame.mixer.music.get_busy():
pygame.mixer.music.pause()
else:
pygame.mixer.music.unpause()
if event.key == pygame.K_s:
volume = pygame.mixer.Sound.get_volume()
if volume > 0:
pygame.mixer.Sound.set_volume(0)
else:
pygame.mixer.Sound.set_volume(1)

Полный пример структуры игры

Код ниже объединяет описанные компоненты в единую систему.

import pygame
import random

WIDTH = 800
HEIGHT = 600
FPS = 60

# Цвета
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)

class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((50, 50))
self.image.fill(GREEN)
self.rect = self.image.get_rect()
self.rect.center = (WIDTH / 2, HEIGHT / 2)
self.speed = 5

def update(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT]:
self.rect.x += self.speed
if keys[pygame.K_UP]:
self.rect.y -= self.speed
if keys[pygame.K_DOWN]:
self.rect.y += self.speed

# Границы экрана
if self.rect.left < 0: self.rect.left = 0
if self.rect.right > WIDTH: self.rect.right = WIDTH
if self.rect.top < 0: self.rect.top = 0
if self.rect.bottom > HEIGHT: self.rect.bottom = HEIGHT

class Enemy(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((30, 30))
self.image.fill(RED)
self.rect = self.image.get_rect()
self.rect.x = random.randrange(WIDTH)
self.rect.y = random.randrange(-100, -40)
self.speed_y = random.randrange(2, 8)

def update(self):
self.rect.y += self.speed_y
if self.rect.top > HEIGHT:
self.rect.x = random.randrange(WIDTH)
self.rect.y = random.randrange(-100, -40)
self.speed_y = random.randrange(2, 8)

# Инициализация
pygame.init()
pygame.mixer.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Игра")
clock = pygame.time.Clock()

# Группы
all_sprites = pygame.sprite.Group()
mobs = pygame.sprite.Group()
player = Player()
all_sprites.add(player)

for i in range(8):
m = Enemy()
all_sprites.add(m)
mobs.add(m)

# Переменные
score = 0
font = pygame.font.SysFont("Arial", 24)
running = True

# Цикл
while running:
clock.tick(FPS)

# События
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Обновление
all_sprites.update()

# Коллизии
hits = pygame.sprite.spritecollide(player, mobs, False)
if hits:
running = False

# Отрисовка
screen.fill(BLACK)
all_sprites.draw(screen)

# Интерфейс
score_text = font.render(f"Счёт: {score}", True, WHITE)
screen.blit(score_text, (10, 10))

pygame.display.flip()

pygame.quit()

Play

Библиотека play — сторонняя библиотека, разработанная для упрощения создания 2D-игр и анимаций, особенно в образовательной среде. Она строится на основе Pygame Zero, но предлагает ещё более минималистичный API, ориентированный на начинающих. Установка проста:

pip install play

play следует императивной модели, в которой программа описывает поведение объектов в виде функций обратного вызова (update, draw). Главный цикл управления выполняется автоматически.

Центральным элементом являются спрайты — двухмерные объекты, обладающие позицией, размером, изображением и свойствами анимации. Спрайт создаётся как экземпляр класса play.new_image() или play.new_box():

ball = play.new_circle(color="red", x=0, y=0, radius=25)

Каждый спрайт имеет атрибуты:

  • x, y — координаты центра.
  • angle — угол поворота.
  • size — масштаб относительно исходного изображения.
  • image — если используется текстура.
  • visible — видимость объекта.

Движение реализуется путём изменения атрибутов спрайтов в функции update(), которая вызывается каждый кадр (обычно 60 раз в секунду):

@play.repeat_forever
def do():
ball.x += 1

Также доступны встроенные методы, такие как move(), point_towards(), rotate().

Обнаружение столкновений (коллизий) выполняется с помощью метода is_touching(other_sprite):

if ball.is_touching(paddle):
ball.color = "blue"

Этот метод проверяет пересечение границ спрайтов (bounding boxes), что достаточно для большинства простых случаев, но не учитывает прозрачные области изображений.

play предоставляет простой доступ к состоянию клавиш:

if play.key_is_pressed("w"):
paddle.y += 5

Поддерживаются буквенные, цифровые и специальные клавиши ("space", "up", "left" и т. д.). Нет необходимости вручную управлять event loop — библиотека делает это автоматически.

Рассмотрим минимальную реализацию игры, где игрок должен перемещать платформу, чтобы поймать падающий шарик.

import play

# Создание объектов
paddle = play.new_box(color="green", x=0, y=-200, width=100, height=20)
ball = play.new_circle(color="red", x=0, y=200, radius=20)
score = play.new_text(words="Счёт: 0", x=0, y=250)

count = 0

@play.repeat_forever
def do():
global count

# Управление платформой
if play.key_is_pressed("left"):
paddle.x -= 5
if play.key_is_pressed("right"):
paddle.x += 5

# Движение шарика
ball.y -= 3

# Проверка коллизии
if ball.is_touching(paddle):
ball.y = 200 # Переместить шарик наверх
count += 1
score.words = f"Счёт: {count}"

# Перезапуск, если шарик упал
if ball.y < -250:
ball.y = 200
count = max(0, count - 1)
score.words = f"Счёт: {count}"

play.start_program()

Этот пример демонстрирует типичную структуру приложения в play: объявление объектов, реакция на события в цикле repeat_forever, использование глобального состояния.


Распространённые механики в Pygame

Обработка столкновений (Collision Detection)

Самая частая задача — проверить, столкнулся ли игрок с препятствием или предметом. В Pygame есть встроенный метод colliderect для прямоугольников (rect) и collidecircle для кругов.

import pygame
import sys

pygame.init()
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()

class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((30, 30))
self.image.fill((0, 255, 0)) # Зеленый
self.rect = self.image.get_rect(center=(400, 300))
self.speed = 5
self.keys_pressed = {'w': False, 'a': False, 's': False, 'd': False}

def update(self):
if self.keys_pressed['w'] and self.rect.top > 0: self.rect.y -= self.speed
if self.keys_pressed['s'] and self.rect.bottom < SCREEN_HEIGHT: self.rect.y += self.speed
if self.keys_pressed['a'] and self.rect.left > 0: self.rect.x -= self.speed
if self.keys_pressed['d'] and self.rect.right < SCREEN_WIDTH: self.rect.x += self.speed

class Obstacle(pygame.sprite.Sprite):
def __init__(self, x, y, w, h):
super().__init__()
self.image = pygame.Surface((w, h))
self.image.fill((255, 0, 0)) # Красный
self.rect = self.image.get_rect(topleft=(x, y))

def main():
all_sprites = pygame.sprite.Group()
obstacles = pygame.sprite.Group()

player = Player()
all_sprites.add(player)

# Создаем препятствия
for i in range(5):
obs = Obstacle(100 + i * 120, 100, 50, 50)
all_sprites.add(obs)
obstacles.add(obs)

running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
key_map = {pygame.K_w: 'w', pygame.K_a: 'a', pygame.K_s: 's', pygame.K_d: 'd'}
if event.key in key_map: player.keys_pressed[key_map[event.key]] = True
elif event.type == pygame.KEYUP:
key_map = {pygame.K_w: 'w', pygame.K_a: 'a', pygame.K_s: 's', pygame.K_d: 'd'}
if event.key in key_map: player.keys_pressed[key_map[event.key]] = False

player.update()

# Проверка столкновений
hits = pygame.sprite.spritecollide(player, obstacles, False)

if hits:
# Если столкнулись — останавливаемся или отменяем движение
# Вариант 1: Просто выводим сообщение (не меняя позицию)
print("Коллизия обнаружена!")
# Вариант 2: Откат позиции (более сложный, требует сохранения координат до движения)
# Для простого примера остановим игрока на месте, если он пытается войти в стену
# Здесь мы просто не будем менять логику движения, но визуально можно подсветить игрока

screen.fill((30, 30, 30))
all_sprites.draw(screen)

# Визуализация коллизии (красная рамка вокруг игрока при ударе)
if hits:
pygame.draw.rect(screen, (255, 255, 0), player.rect, 3)

pygame.display.flip()
clock.tick(60)

pygame.quit()
sys.exit()

if __name__ == "__main__":
main()

Система частиц (Particle System)

Частицы используются для эффектов взрыва, следов от шагов, магии или дождя. Это множество маленьких объектов, которые живут короткое время, уменьшаются или меняют прозрачность.

import pygame
import random
import math

# ... (код инициализации и класса Player из предыдущего примера можно скопировать сюда) ...

class Particle(pygame.sprite.Sprite):
def __init__(self, x, y, color, speed_range, size_range):
super().__init__()
self.size = random.randint(size_range[0], size_range[1])
self.image = pygame.Surface((self.size, self.size))
self.image.fill(color)
self.rect = self.image.get_rect(center=(x, y))

# Случайное направление разлета
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(speed_range[0], speed_range[1])
self.vx = math.cos(angle) * speed
self.vy = math.sin(angle) * speed

self.life = 1.0 # Жизнь частицы от 1.0 до 0.0
self.decay = random.uniform(0.02, 0.05) # Скорость затухания

def update(self):
self.rect.x += self.vx
self.rect.y += self.vy
self.life -= self.decay

# Уменьшаем размер по мере жизни
new_size = int(self.size * self.life)
if new_size <= 0:
self.kill() # Удаляем спрайт из всех групп
else:
self.image = pygame.Surface((new_size, new_size))
self.image.fill((255, 255, 255)) # Можно сделать цветным, используя alpha
# Для простоты оставим белый, но в реальном проекте лучше использовать Surface.set_alpha
self.rect.center = (self.rect.x + self.vx, self.rect.y + self.vy) # Корректировка центра

def create_explosion(x, y, count=20):
particles = []
colors = [(255, 165, 0), (255, 0, 0), (255, 255, 0)]
for _ in range(count):
p = Particle(x, y, random.choice(colors), (2, 5), (3, 8))
particles.append(p)
return particles

# Пример использования внутри цикла main:
# При клике мыши создаем частицы
# mouse_buttons = pygame.mouse.get_pressed()
# if mouse_buttons[0]: # ЛКМ
# mx, my = pygame.mouse.get_pos()
# explosion = create_explosion(mx, my)
# all_sprites.add(explosion)

Тайловый фон (Tilemap)

Для создания уровня без рисования каждого пикселя вручную используется сетка тайлов. Каждый элемент сетки — это картинка (тайл), которая повторяется.

import pygame

TILE_SIZE = 64
MAP_WIDTH = 20 # Количество тайлов по ширине
MAP_HEIGHT = 15 # Количество тайлов по высоте

# Генерация карты (0 - земля, 1 - стена, 2 - вода)
# В реальности это загружается из JSON или текстового файла
simple_map = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 2, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 2, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,......], # Упрощено
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]

# Загрузка тайлов (для примера используем цвета)
tiles = {
0: pygame.Surface((TILE_SIZE, TILE_SIZE)), # Земля
1: pygame.Surface((TILE_SIZE, TILE_SIZE)), # Стена
2: pygame.Surface((TILE_SIZE, TILE_SIZE)) # Вода
}

for key in tiles:
if key == 0: tiles[key].fill((34, 139, 34)) # Forest Green
elif key == 1: tiles[key].fill((105, 105, 105)) # Gray
else: tiles[key].fill((65, 105, 225)) # Royal Blue

def draw_map(screen, map_data):
for row_idx, row in enumerate(map_data):
for col_idx, tile_type in enumerate(row):
screen.blit(tiles[tile_type], (col_idx * TILE_SIZE, row_idx * TILE_SIZE))

# ... в функции main() после инициализации ...
# draw_map(screen, simple_map)

Камера (Camera Follow)

Вместо того чтобы двигать игрока по бесконечному миру, мы двигаем мир (фон, врагов) в противоположную сторону от игрока. Это позволяет создавать уровни больше экрана.

import pygame
import sys

pygame.init()
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()

# Размеры "мира" (больше экрана)
WORLD_WIDTH = 2000
WORLD_HEIGHT = 2000

class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((30, 30))
self.image.fill((0, 255, 0))
self.rect = self.image.get_rect(center=(100, 100)) # Начальная позиция в мире
self.speed = 5
self.keys_pressed = {'w': False, 'a': False, 's': False, 'd': False}

def update(self):
if self.keys_pressed['w'] and self.rect.top > 0: self.rect.y -= self.speed
if self.keys_pressed['s'] and self.rect.bottom < WORLD_HEIGHT: self.rect.y += self.speed
if self.keys_pressed['a'] and self.rect.left > 0: self.rect.x -= self.speed
if self.keys_pressed['d'] and self.rect.right < WORLD_WIDTH: self.rect.x += self.speed

class Camera:
def __init__(self, width, height):
self.camera = pygame.Rect(0, 0, width, height)
self.width = width
self.height = height

def apply(self, entity):
"""Применяет смещение камеры к сущности."""
return entity.rect.move(self.camera.topleft)

def update(self, target):
"""Центрирует камеру на цели."""
x = -target.rect.centerx + int(SCREEN_WIDTH / 2)
y = -target.rect.centery + int(SCREEN_HEIGHT / 2)

# Ограничиваем камеру границами мира (чтобы не видеть пустоту за краями)
x = min(0, max(-(self.width - SCREEN_WIDTH), x))
y = min(0, max(-(self.height - SCREEN_HEIGHT), y))

self.camera.topleft = x, y

def main():
all_sprites = pygame.sprite.Group()

player = Player()
all_sprites.add(player)

# Создадим несколько объектов для проверки работы камеры
for i in range(5):
obs = pygame.sprite.Sprite()
obs.image = pygame.Surface((40, 40))
obs.image.fill((255, 165, 0))
obs.rect = obs.image.get_rect(center=(500 + i * 300, 500))
all_sprites.add(obs)

camera = Camera(WORLD_WIDTH, WORLD_HEIGHT)

running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
key_map = {pygame.K_w: 'w', pygame.K_a: 'a', pygame.K_s: 's', pygame.K_d: 'd'}
if event.key in key_map: player.keys_pressed[key_map[event.key]] = True
elif event.type == pygame.KEYUP:
key_map = {pygame.K_w: 'w', pygame.K_a: 'a', pygame.K_s: 's', pygame.K_d: 'd'}
if event.key in key_map: player.keys_pressed[key_map[event.key]] = False

player.update()
camera.update(player)

screen.fill((30, 30, 30))

# Рисуем все спрайты с учетом смещения камеры
for sprite in all_sprites:
screen.blit(sprite.image, camera.apply(sprite).rect)

pygame.display.flip()
clock.tick(60)

pygame.quit()
sys.exit()

if __name__ == "__main__":
main()

Простой инвентарь (Grid UI)

Создание сетки для отображения предметов. Это база для RPG или стратегий. Мы используем Rect для определения слотов и проверку кликов мыши.

import pygame
import sys

pygame.init()
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()

ITEM_SIZE = 50
GRID_ROWS = 4
GRID_COLS = 5
START_X = 50
START_Y = 50

class Inventory:
def __init__(self):
self.slots = [None] * (GRID_ROWS * GRID_COLS)
self.slot_rects = []

# Генерируем прямоугольники слотов
for r in range(GRID_ROWS):
for c in range(GRID_COLS):
rect = pygame.Rect(
START_X + c * (ITEM_SIZE + 5),
START_Y + r * (ITEM_SIZE + 5),
ITEM_SIZE, ITEM_SIZE
)
self.slot_rects.append(rect)

def draw(self, surface):
# Рисуем фон инвентаря
pygame.draw.rect(surface, (40, 40, 40), (START_X - 5, START_Y - 5,
GRID_COLS * (ITEM_SIZE + 5) + 5,
GRID_ROWS * (ITEM_SIZE + 5) + 5),
border_radius=5)

# Рисуем слоты
for i, rect in enumerate(self.slot_rects):
color = (70, 70, 70) if self.slots[i] is None else (100, 100, 100)
pygame.draw.rect(surface, color, rect, 2)

if self.slots[i]:
# Рисуем предмет (условно цветной квадрат)
item_color = self.slots[i]['color']
pygame.draw.rect(surface, item_color, rect.inflate(-4, -4))

# Текст названия (опционально)
font = pygame.font.SysFont('Arial', 10)
text = font.render(str(i), True, (255, 255, 255))
surface.blit(text, (rect.x + 2, rect.y + 2))

def handle_click(self, pos):
"""Проверяет клик по слоту"""
for i, rect in enumerate(self.slot_rects):
if rect.collidepoint(pos):
print(f"Клик по слоту {i}")
# Логика: взять предмет, положить предмет и т.д.
if self.slots[i] is None:
# Пример добавления предмета
self.slots[i] = {'id': i, 'color': (255, 0, 0)}
else:
self.slots[i] = None

def main():
inventory = Inventory()
running = True

while running:
mouse_pos = pygame.mouse.get_pos()

for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # Левый клик
inventory.handle_click(mouse_pos)

screen.fill((200, 200, 200))
inventory.draw(screen)

# Визуализация курсора над слотом
hover_slot = None
for i, rect in enumerate(inventory.slot_rects):
if rect.collidepoint(mouse_pos):
hover_slot = i
break

if hover_slot is not None:
pygame.draw.rect(screen, (255, 255, 0), inventory.slot_rects[hover_slot], 2)

pygame.display.flip()
clock.tick(60)

pygame.quit()
sys.exit()

if __name__ == "__main__":
main()

Анимация спрайта (Sprite Sheet)

Использование одного изображения (спрайт-листа), где содержатся все кадры анимации, и переключение видимых частей.

Примечание: Для примера мы генерируем картинку программно, но в реальном проекте вы загружаете файл .png.

import pygame
import sys

pygame.init()
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()

class AnimatedPlayer(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
# Имитация спрайт-листа: 4 кадра по 30px ширины, высота 30px
# В реальности: image = pygame.image.load("walk.png")
sheet_width = 120 # 4 кадра * 30px
sheet_height = 30

# Создаем тестовую картинку (полоски разного цвета для разных кадров)
self.sheet = pygame.Surface((sheet_width, sheet_height))
colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)]
for i, color in enumerate(colors):
self.sheet.subsurface(pygame.Rect(i * 30, 0, 30, 30)).fill(color)

self.frames = []
frame_size = 30
for i in range(len(colors)):
# Вырезаем кадр из листа
frame = self.sheet.subsurface(pygame.Rect(i * frame_size, 0, frame_size, frame_size)).copy()
self.frames.append(frame)

self.frame_index = 0
self.current_frame = self.frames[self.frame_index]
self.image = self.current_frame
self.rect = self.image.get_rect(center=(400, 300))

self.animation_speed = 0.15 # Скорость смены кадров (меньше = медленнее)
self.timer = 0
self.is_moving = False

def update(self):
# Если игрок движется, включаем анимацию
if self.is_moving:
self.timer += 1
if self.timer >= 10: # Меняем кадр каждые 10 кадров (примерно 6 раз в секунду при 60 FPS)
self.timer = 0
self.frame_index = (self.frame_index + 1) % len(self.frames)
self.current_frame = self.frames[self.frame_index]
self.image = self.current_frame.copy() # Важно копировать, если меняются свойства
self.rect.center = self.rect.center # Обновляем центр, чтобы rect не сместился
else:
# Статичный кадр (первый или последний)
pass

def set_moving(self, moving):
self.is_moving = moving

def main():
player = AnimatedPlayer()
all_sprites = pygame.sprite.Group()
all_sprites.add(player)

# Добавим простой текст для управления
font = pygame.font.SysFont('Arial', 20)
text = font.render("Держи W/A/S/D для движения", True, (0, 0, 0))

running = True
keys_pressed = {'w': False, 'a': False, 's': False, 'd': False}

while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
key_map = {pygame.K_w: 'w', pygame.K_a: 'a', pygame.K_s: 's', pygame.K_d: 'd'}
if event.key in key_map: keys_pressed[key_map[event.key]] = True
elif event.type == pygame.KEYUP:
key_map = {pygame.K_w: 'w', pygame.K_a: 'a', pygame.K_s: 's', pygame.K_d: 'd'}
if event.key in key_map: keys_pressed[key_map[event.key]] = False

# Логика движения
speed = 5
if keys_pressed['w']: player.rect.y -= speed
if keys_pressed['s']: player.rect.y += speed
if keys_pressed['a']: player.rect.x -= speed
if keys_pressed['d']: player.rect.x += speed

# Проверка: движется ли игрок?
moved = any([keys_pressed[k] for k in keys_pressed])
player.set_moving(moved)
player.update()

screen.fill((240, 240, 240))
all_sprites.draw(screen)
screen.blit(text, (10, 10))

pygame.display.flip()
clock.tick(60)

pygame.quit()
sys.exit()

if __name__ == "__main__":
main()

Пауза и Меню (Game State Management)

Управление состоянием игры позволяет переключаться между "Игрой", "Паузой" и "Меню". Это реализуется через флаг состояния (game_state).

import pygame
import sys

pygame.init()
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()

# Состояния игры
STATE_PLAYING = "playing"
STATE_PAUSED = "paused"
STATE_GAME_OVER = "game_over"

class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((30, 30))
self.image.fill((0, 255, 0))
self.rect = self.image.get_rect(center=(400, 300))
self.hp = 100
self.speed = 5
self.keys_pressed = {'w': False, 'a': False, 's': False, 'd': False}

def update(self):
if not self.keys_pressed['w'] and not self.keys_pressed['a'] and \
not self.keys_pressed['s'] and not self.keys_pressed['d']:
return # Нет движения

if self.keys_pressed['w'] and self.rect.top > 0: self.rect.y -= self.speed
if self.keys_pressed['s'] and self.rect.bottom < SCREEN_HEIGHT: self.rect.y += self.speed
if self.keys_pressed['a'] and self.rect.left > 0: self.rect.x -= self.speed
if self.keys_pressed['d'] and self.rect.right < SCREEN_WIDTH: self.rect.x += self.speed

def draw_text(surface, text, size, color, x, y, center=False):
font = pygame.font.SysFont('Arial', size)
text_surface = font.render(text, True, color)
rect = text_surface.get_rect()
if center:
rect.center = (x, y)
else:
rect.topleft = (x, y)
surface.blit(text_surface, rect)

def main():
player = Player()
all_sprites = pygame.sprite.Group()
all_sprites.add(player)

game_state = STATE_PLAYING

running = True
keys_pressed = {'w': False, 'a': False, 's': False, 'd': False}

while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
if game_state == STATE_PLAYING:
game_state = STATE_PAUSED
elif game_state == STATE_PAUSED:
game_state = STATE_PLAYING

elif event.key == pygame.K_r and game_state == STATE_GAME_OVER:
# Перезапуск
player.hp = 100
player.rect.center = (400, 300)
game_state = STATE_PLAYING

key_map = {pygame.K_w: 'w', pygame.K_a: 'a', pygame.K_s: 's', pygame.K_d: 'd'}
if event.key in key_map: keys_pressed[key_map[event.key]] = True

elif event.type == pygame.KEYUP:
key_map = {pygame.K_w: 'w', pygame.K_a: 'a', pygame.K_s: 's', pygame.K_d: 'd'}
if event.key in key_map: keys_pressed[key_map[event.key]] = False

# Логика зависит от состояния
if game_state == STATE_PLAYING:
player.update()

# Имитация получения урона (для примера - случайный удар раз в секунду)
import random
if random.random() < 0.02: # 2% шанс в кадр
player.hp -= 10
if player.hp <= 0:
game_state = STATE_GAME_OVER

elif game_state == STATE_PAUSED:
pass # Логика игры не обновляется

# Отрисовка
screen.fill((30, 30, 30))

if game_state == STATE_PLAYING or game_state == STATE_PAUSED:
all_sprites.draw(screen)
# Рисуем здоровье
draw_text(screen, f"HP: {player.hp}", 24, (255, 255, 255), 20, 20)

if game_state == STATE_PAUSED:
# Затемнение фона
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
overlay.set_alpha(128)
overlay.fill((0, 0, 0))
screen.blit(overlay, (0, 0))
draw_text(screen, "ПАУЗА", 60, (255, 255, 255), SCREEN_WIDTH//2, SCREEN_HEIGHT//2, center=True)
draw_text(screen, "Нажмите ESC для продолжения", 20, (200, 200, 200), SCREEN_WIDTH//2, SCREEN_HEIGHT//2 + 50, center=True)

if game_state == STATE_GAME_OVER:
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
overlay.set_alpha(200)
overlay.fill((0, 0, 0))
screen.blit(overlay, (0, 0))
draw_text(screen, "GAME OVER", 60, (255, 0, 0), SCREEN_WIDTH//2, SCREEN_HEIGHT//2 - 50, center=True)
draw_text(screen, "Нажмите R для рестарта", 24, (255, 255, 255), SCREEN_WIDTH//2, SCREEN_HEIGHT//2 + 50, center=True)

pygame.display.flip()
clock.tick(60)

pygame.quit()
sys.exit()

if __name__ == "__main__":
main()

Система здоровья с визуальным эффектом (Hit Flash)

Когда персонаж получает урон, он должен мигнуть красным или белым цветом. Это делается через таймер и изменение цвета поверхности спрайта.

import pygame
import sys

pygame.init()
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()

class HealthPlayer(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((40, 40))
self.image.fill((0, 255, 0)) # Зеленый
self.original_color = (0, 255, 0)
self.rect = self.image.get_rect(center=(400, 300))

self.hp = 100
self.max_hp = 100
self.is_hit = False
self.hit_timer = 0
self.hit_duration = 10 # Кадров (около 0.16 сек при 60 FPS)

def take_damage(self, amount):
"""Получение урона"""
self.hp -= amount
if self.hp < 0: self.hp = 0

# Включаем эффект удара
self.is_hit = True
self.hit_timer = self.hit_duration

def update(self):
if self.is_hit:
self.hit_timer -= 1
# Мигаем: каждый четный кадр меняем цвет на белый, нечетный - красный
if self.hit_timer % 2 == 0:
self.image.fill((255, 255, 255)) # Белый
else:
self.image.fill((255, 0, 0)) # Красный

if self.hit_timer <= 0:
self.is_hit = False
self.image.fill(self.original_color)
else:
# Если есть движение (упрощено), можно добавить анимацию, но здесь только удар
pass

def main():
player = HealthPlayer()
all_sprites = pygame.sprite.Group()
all_sprites.add(player)

running = True
mouse_buttons = [False, False, False]

while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Клик мышью наносит урон игроку (для теста)
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # ЛКМ
player.take_damage(20)

player.update()

screen.fill((40, 40, 40))
all_sprites.draw(screen)

# Отображение полоски здоровья
bar_width = 200
bar_height = 20
bar_x = 50
bar_y = 50

# Фон полоски
pygame.draw.rect(screen, (100, 100, 100), (bar_x, bar_y, bar_width, bar_height))
# Текущее здоровье
hp_width = int(bar_width * (player.hp / player.max_hp))
hp_color = (0, 255, 0) if player.hp > 30 else (255, 0, 0)
pygame.draw.rect(screen, hp_color, (bar_x, bar_y, hp_width, bar_height))

# Текст
font = pygame.font.SysFont('Arial', 16)
text = font.render(f"HP: {player.hp}/{player.max_hp}", True, (255, 255, 255))
screen.blit(text, (bar_x, bar_y - 20))

pygame.display.flip()
clock.tick(60)

pygame.quit()
sys.exit()

if __name__ == "__main__":
main()

Сохранение и загрузка данных (JSON)

Для сохранения прогресса (позиция, здоровье, инвентарь) используется модуль json. Это стандартный способ хранения структурированных данных в текстовом виде.

import pygame
import sys
import json
import os

SAVE_FILE = "save_game.json"

# ... (код инициализации Pygame и класса Player из предыдущих примеров) ...

class SaveablePlayer(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((30, 30))
self.image.fill((0, 255, 0))
self.rect = self.image.get_rect(center=(400, 300))
self.hp = 100
self.level = 1
self.score = 0

def save_data(self):
"""Создает словарь данных для сохранения"""
return {
"position": {"x": self.rect.centerx, "y": self.rect.centery},
"hp": self.hp,
"level": self.level,
"score": self.score
}

def load_data(self, data):
"""Загружает данные из словаря"""
if "position" in data:
self.rect.centerx = data["position"]["x"]
self.rect.centery = data["position"]["y"]
if "hp" in data:
self.hp = data["hp"]
if "level" in data:
self.level = data["level"]
if "score" in data:
self.score = data["score"]

def save_game(player):
try:
data = player.save_data()
with open(SAVE_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
print("Игра сохранена успешно.")
except Exception as e:
print(f"Ошибка сохранения: {e}")

def load_game(player):
if not os.path.exists(SAVE_FILE):
print("Файл сохранения не найден.")
return False

try:
with open(SAVE_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
player.load_data(data)
print("Игра загружена.")
return True
except Exception as e:
print(f"Ошибка загрузки: {e}")
return False

def main():
player = SaveablePlayer()
all_sprites = pygame.sprite.Group()
all_sprites.add(player)

# Проверка наличия сохранения при старте
# Если хотите всегда начинать заново, закомментируйте строку ниже
# load_game(player)

running = True
keys_pressed = {'w': False, 'a': False, 's': False, 'd': False}

while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_F5:
save_game(player)

key_map = {pygame.K_w: 'w', pygame.K_a: 'a', pygame.K_s: 's', pygame.K_d: 'd'}
if event.key in key_map: keys_pressed[key_map[event.key]] = True

elif event.type == pygame.KEYUP:
key_map = {pygame.K_w: 'w', pygame.K_a: 'a', pygame.K_s: 's', pygame.K_d: 'd'}
if event.key in key_map: keys_pressed[key_map[event.key]] = False

# Обновление логики
speed = 5
if keys_pressed['w'] and player.rect.top > 0: player.rect.y -= speed
if keys_pressed['s'] and player.rect.bottom < SCREEN_HEIGHT: player.rect.y += speed
if keys_pressed['a'] and player.rect.left > 0: player.rect.x -= speed
if keys_pressed['d'] and player.rect.right < SCREEN_WIDTH: player.rect.x += speed

# Пример изменения данных
player.score += 1

# Отрисовка
screen.fill((30, 30, 30))
all_sprites.draw(screen)

font = pygame.font.SysFont('Arial', 20)
info_text = f"Score: {player.score} | F5: Save"
screen.blit(font.render(info_text, True, (255, 255, 255)), (20, 20))

pygame.display.flip()
clock.tick(60)

# Автоматическое сохранение при выходе
save_game(player)
pygame.quit()
sys.exit()

if __name__ == "__main__":
main()